/** ***************************************************************************
 * CoffeeParty.hpp
 * 2012 CoffeeParty Ltd.
 * Date Created: 2012-07-21                                  format YYYY-MM-DD
 * Revision Id: $Id$
 ******************************************************************************
 */

#ifndef _COFFEE_PARTY_HPP_
#define _COFFEE_PARTY_HPP_

#include "CoffeeCommons.hpp"
#include "CoffeeTypes.hpp"
#include "CoffeeLogger.hpp"
#include "CoffeeString.hpp"
#include "CoffeeRect.hpp"

#include <algorithm>
#include <vector>

struct CoffeeParams
{
public:
    CoffeeParams()
    : c_width(640), c_height(480), c_bitspp(32), c_flags(SDL_OPENGL),
      c_ogl_redbit(8), c_ogl_greenbit(8), c_ogl_bluebit(8),
      c_ogl_alphabit(8), c_ogl_depthbit(16), c_ogl_doublebuffer(1) { }

    ~CoffeeParams() { }

    c_const_uint width() const
    {
        return c_width;
    }

    c_void width(c_uint w)
    {
        c_width = w;
    }

    c_const_uint height() const
    {
        return c_height;
    }

    c_void height(c_uint h)
    {
        c_height = h;
    }

    c_const_ushort bitspp() const
    {
        return c_bitspp;
    }

    c_void bitspp(c_ushort b)
    {
        c_bitspp = b;
    }

    c_const_ushort ogl_redbit() const
    {
        return c_ogl_redbit;
    }

    c_const_ushort ogl_greenbit() const
    {
        return c_ogl_greenbit;
    }

    c_const_ushort ogl_bluebit() const
    {
        return c_ogl_bluebit;
    }

    c_const_ushort ogl_alphabit() const
    {
        return c_ogl_alphabit;
    }

    c_const_ushort ogl_depthbit() const
    {
        return c_ogl_depthbit;
    }

    c_const_ushort ogl_doublebuffer() const
    {
        return c_ogl_doublebuffer;
    }

protected:
    c_uint c_width;
    c_uint c_height;
    c_uint c_bitspp;
    c_ushort c_flags;

    c_ushort c_ogl_redbit;
    c_ushort c_ogl_greenbit;
    c_ushort c_ogl_bluebit;
    c_ushort c_ogl_alphabit;
    c_ushort c_ogl_depthbit;
    c_ushort c_ogl_doublebuffer;
};

#include "SugarImage.hpp"

struct CoffeeParty
{
public:
    CoffeeParty()
    : internal_screen_width(640), internal_screen_height(480)
    {
        info();
    }

    CoffeeParty(int screen_width, int screen_height)
    : internal_screen_width(screen_width), internal_screen_height(screen_height)
    {
        info();
    }

    CoffeeParty(c_dimension<c_int>& dimension)
    : internal_screen_width(dimension.x), internal_screen_height(dimension.y)
    {
        info();
    }

    ~CoffeeParty() { }

    c_void setInternalDimension(int screen_width, int screen_height)
    {
        internal_screen_width = screen_width;
        internal_screen_height = screen_height;
    }

    c_void setInternalDimension(c_dimension<c_int>& dimension)
    {
        internal_screen_width = dimension.x;
        internal_screen_height = dimension.y;
    }

    c_void setViewport(c_int x = 0, c_int y = 0, c_int w = 480, c_int h = 272)
    {
        glViewport(x, y, w, h);
    }

    c_void setViewport(c_position<c_int>& pos, c_dimension<c_uint>& size)
    {
        glViewport(pos.x, pos.y, size.x, size.y);
    }

    const c_dimension<c_uint> getCurrentTargetSize() const
    {
        return c_dimension<c_uint>((c_uint)internal_screen_width, (c_uint)internal_screen_height);
    }

    c_void setViewport(c_rect<c_int>& area)
    {
        c_rect<c_int> vp = area;
        c_rect<c_int> rendert(0, 0, internal_screen_width, internal_screen_height);
        vp.clipAgainst(rendert);

        if (vp.getHeight() > 0 && vp.getWidth() > 0)
            glViewport(vp.UpperLeftCorner.x,
                internal_screen_height - vp.UpperLeftCorner.y - vp.getHeight(),
                vp.getWidth(), vp.getHeight());
    }

    c_void loadProjectionMat()
    {
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
    }

    c_void loadModelViewMat()
    {
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
    }

    c_void enable(c_uint token)
    {
        glEnable(token);
    }

    c_void enable2DRendering()
    {
        glEnable(GL_TEXTURE_2D);
    }

    c_void enableZBuffer()
    {
        glDepthMask(GL_TRUE);
    }

    c_void disable(c_uint token)
    {
        glDisable(token);
    }

    c_void disable2DRendering()
    {
        glDisable(GL_TEXTURE_2D);
    }

    c_void disableZBuffer()
    {
        glDepthMask(GL_FALSE);
    }

    c_void pushMat()
    {
        glPushMatrix();
    }

    c_void pushAttrib(c_uint token)
    {
        glPushAttrib(token);
    }

    c_void popMat()
    {
        glPopMatrix();
    }

    c_void popAttrib()
    {
        glPopAttrib();
    }

    c_void clearColor(c_float r = 0.0f, c_float g = 0.0f, c_float b = 0.0f, c_float a = 0.0f)
    {
        glClearColor(r, g, b, a);
    }

    c_void clearColor(c_float color[4])
    {
        glClearColor(color[0], color[1], color[2], color[3]);
    }

    c_void clearColor(c_double color[4])
    {
        glClearColor(color[0], color[1], color[2], color[3]);
    }

    c_void clearColorVast()
    {
        glClearColor(0.0f, 0.6823529411764706f, 1.0f, 1.0f);
    }

    c_void clearDepth(c_float d = 1.0f)
    {
        glClearDepth(d);
    }

    c_void clearStencil(c_int s = 0)
    {
        glClearStencil(s);
    }

    c_void clearAll(c_int type = 0)
    {
        if (type == 0)
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
        else if (type == 1)
            glClear(GL_COLOR_BUFFER_BIT);
        else c_print("Invalid clear options");
    }

    c_void clearBuffers(c_bool back, c_bool zbuffer, c_bool stencil)
    {
        GLbitfield flags = 0;
        if (back)
            flags |= GL_COLOR_BUFFER_BIT;

        if (zbuffer)
        {
            glDepthMask(GL_TRUE);
            flags |= GL_DEPTH_BUFFER_BIT;
        }

        if (stencil)
            flags |= GL_STENCIL_BUFFER_BIT;

        if (flags)
            glClear(flags);

    }

    c_void ortho2d(c_float top, c_float left, c_float bottom, c_float right)
    {
        gluOrtho2D(left, right, bottom, top);
    }

    c_void toOrtho(c_int screen_width, c_int screen_height)
    {
        setViewport(0, 0, screen_width, screen_height);
        loadProjectionMat();
        glOrtho(0, screen_width, screen_height, 0, -1, 1);
        loadModelViewMat();
    }

    c_void toOrtho(c_dimension<c_uint>& screen)
    {
        setViewport(0, 0, screen.x, screen.y);
        loadProjectionMat();
        glOrtho(0, screen.x, screen.y, 0, -1, 1);
        loadModelViewMat();
    }

    c_void perspective(c_float fovy, c_float aspectratio, c_float znear, c_float zfar)
    {
        gluPerspective(fovy, aspectratio, znear, zfar);
    }

    c_void toPerspective(c_int screen_width, c_int screen_height)
    {
        setViewport(0, 0, screen_width, screen_height);
        loadProjectionMat();
        perspective(60.0f, (c_float)screen_width / screen_height, 1.0f, 1000.0f);
        loadModelViewMat();
    }

    c_void toPerspective(c_dimension<c_uint>& screen)
    {
        setViewport(0, 0, screen.x, screen.y);
        loadProjectionMat();
        perspective(60.0f, (c_float)screen.x / screen.y, 1.0f, 1000.0f);
        loadModelViewMat();
    }

    c_void setCamera(c_float posx, c_float posy, c_float posz, c_float targetx, c_float targety,
        c_float targetz)
    {
        loadModelViewMat();
        gluLookAt(posx, posy, posz, targetx, targety, targetz, 0, 1, 0);
    }

    c_void fixSDLFace()
    {
        ///< SDL defaults set it to GL_CCW
        glFrontFace(GL_CW);
    }

    c_void info()
    {
        c_const_bytep tmp;
        c_const_bytep tok;

        tmp = (c_const_bytep)glGetString(GL_VENDOR);
        if (tmp)
            vendor = tmp;
        else return;

        tmp = (c_const_bytep)glGetString(GL_RENDERER);
        if (tmp)
            renderer = tmp;
        else return;

        tmp = (c_const_bytep)glGetString(GL_VERSION);
        if (tmp)
            glver = tmp;
        else return;
#if !defined(__WIN32) || !defined(GL_SHADING_LANGUAGE_VERSION)
        tmp = (c_const_bytep)glGetString(GL_SHADING_LANGUAGE_VERSION);
        if (tmp)
            glslver = tmp;
        else glslver = "";
#endif

        tmp = (c_const_bytep)glGetString(GL_EXTENSIONS);
        if (tmp)
        {
            tok = strtok((c_bytep)tmp, " ");
            while (tok)
            {
                extension.push_back(tok);
                tok = strtok(0, " ");
            }
        }

        std::sort(extension.begin(), extension.end());
    }

    c_bool checkError()
    {
        GLenum error = glGetError();
        switch (error)
        {
        case GL_NO_ERROR:
            return false;
        case GL_INVALID_ENUM:
            c_print("GL_INVALID_ENUM");
            break;
        case GL_INVALID_VALUE:
            c_print("GL_INVALID_VALUE");
            break;
        case GL_INVALID_OPERATION:
            c_print("GL_INVALID_OPERATION");
            break;
        case GL_STACK_OVERFLOW:
            c_print("GL_STACK_OVERFLOW");
            break;
        case GL_STACK_UNDERFLOW:
            c_print("GL_STACK_UNDERFLOW");
            break;
        case GL_OUT_OF_MEMORY:
            c_print("GL_OUT_OF_MEMORY");
            break;
        case GL_TABLE_TOO_LARGE:
            c_print("GL_TABLE_TOO_LARGE");
            break;
#if !defined(__WIN32) || !defined(GL_INVALID_FRAMEBUFFER_OPERATION_EXT)
        case GL_INVALID_FRAMEBUFFER_OPERATION_EXT:
            c_print("GL_INVALID_FRAMEBUFFER_OPERATION");
            break;
#endif
        }
        return true;
    }

    c_void printExtensions()
    {
        c_print("Number of Extensions: %d", extension.size());
        for (std::vector<c_string>::iterator it = extension.begin();
             it != extension.end(); it++)
            c_print("%s", ((c_string)*it).c_str());
    }

    c_bool isExtensionSupported(c_const_string& ext)
    {
        std::vector<c_string>::const_iterator it = extension.begin();
        std::vector<c_string>::const_iterator endIt = extension.end();

        while (it != endIt)
        {
            if (ext == *it)
                return true;
            else ++it;
        }
        return false;
    }

    c_bool isVBOSupported()
    {
        if (isExtensionSupported("GL_ARB_vertex_buffer_object"))
            return true;
        else return false;
    }

    c_bool isVAOSupported()
    {
        if (isExtensionSupported("GL_ARB_vertex_array_object"))
            return true;
        else return false;
    }

    c_dimension<c_uint> maxSupportedTextureSize() const
    {
        return c_dimension<c_uint>(0x4000, 0x4000);
    }

    c_bool checkPowerOfTwo(const c_dimension<c_uint>& size)
    {
        c_int width = (size.x & (size.x - 1));
        c_int height = (size.y & (size.y - 1));

        if (width > 0 || height > 0)
            return false;

        return true;
    }

    c_void draw2dRectangle(c_color color, const c_rect<c_int>& pos, const c_rect<c_int>* clip)
    {
        c_rect<c_int> poss = pos;

        if (clip)
            poss.clipAgainst(*clip);

        if (!pos.isValid())
            return;

        glColor4ub(color.red(), color.green(), color.blue(), color.alpha());
        glRectf(GLfloat(pos.UpperLeftCorner.x), GLfloat(pos.UpperLeftCorner.y),
            GLfloat(pos.LowerRightCorner.x), GLfloat(pos.LowerRightCorner.y));
    }

    c_void draw2DRectangle(const c_rect<c_int>& pos, const c_rect<c_int>* clip)
    {
        c_rect<c_int> poss = pos;

        if (clip)
            poss.clipAgainst(*clip);

        if (!pos.isValid())
            return;

        glBegin(GL_QUADS);
        glVertex2f(GLfloat(pos.UpperLeftCorner.x), GLfloat(pos.UpperLeftCorner.y));
        glVertex2f(GLfloat(pos.LowerRightCorner.x), GLfloat(pos.UpperLeftCorner.y));
        glVertex2f(GLfloat(pos.LowerRightCorner.x), GLfloat(pos.LowerRightCorner.y));
        glVertex2f(GLfloat(pos.UpperLeftCorner.x), GLfloat(pos.LowerRightCorner.y));
        glEnd();
    }

    c_void draw2dLine(c_position<c_int>& start, c_position<c_int>& end, c_color color)
    {
        glBegin(GL_LINES);
        glColor4ub(color.red(), color.green(), color.blue(), color.alpha());
        glVertex2f(GLfloat(start.x), GLfloat(start.y));
        glVertex2f(GLfloat(end.x),   GLfloat(end.y));
        glEnd();
    }

    c_void drawPixel(c_uint x, c_uint y, const c_color& color)
    {
        c_dimension<c_uint> renderTargetSize = c_dimension<c_uint>(internal_screen_width, internal_screen_height);
        if (x > (c_uint)renderTargetSize.x || y > (c_uint)renderTargetSize.y)
            return;

        glBegin(GL_POINTS);
        glColor4ub(color.red(), color.green(), color.blue(), color.alpha());
        glVertex2i(x, y);
        glEnd();
    }

    c_void enable2dModeFunc(c_bool alpha, c_bool texture, c_bool alphaChannel)
    {
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        if (alphaChannel || alpha)
        {
            glEnable(GL_BLEND);
            glEnable(GL_ALPHA_TEST);
            glAlphaFunc(GL_GREATER, 0.0f);
        }
        else
        {
            glDisable(GL_BLEND);
            glDisable(GL_ALPHA_TEST);
        }

        if (texture)
        {
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        }
    }

private:
    c_string vendor;
    c_string renderer;
    c_string glver;
    c_string glslver;

    c_uint internal_screen_width;
    c_uint internal_screen_height;

    std::vector<c_string> extension;
};

#endif /** _COFFEE_PARTY_HPP_ */
